package edu.washington.cs.workflow.analysis.guistructure;

import java.io.Writer;

import com.ibm.wala.shrikeBT.ConstantInstruction;
import com.ibm.wala.shrikeBT.Constants;
import com.ibm.wala.shrikeBT.Disassembler;
import com.ibm.wala.shrikeBT.DupInstruction;
import com.ibm.wala.shrikeBT.IInstruction;
import com.ibm.wala.shrikeBT.Instruction;
import com.ibm.wala.shrikeBT.InvokeInstruction;
import com.ibm.wala.shrikeBT.LoadInstruction;
import com.ibm.wala.shrikeBT.MethodData;
import com.ibm.wala.shrikeBT.MethodEditor;
import com.ibm.wala.shrikeBT.ReturnInstruction;
import com.ibm.wala.shrikeBT.SwapInstruction;
import com.ibm.wala.shrikeBT.ThrowInstruction;
import com.ibm.wala.shrikeBT.Util;
import com.ibm.wala.shrikeBT.MethodEditor.Output;
import com.ibm.wala.shrikeBT.analysis.Verifier;
import com.ibm.wala.shrikeBT.shrikeCT.ClassInstrumenter;
import com.ibm.wala.shrikeCT.ClassReader;
import com.ibm.wala.shrikeCT.ClassWriter;

import edu.washington.cs.workflow.instrument.AbstractInstrumenter;
import edu.washington.cs.workflow.instrument.ExecTracer;
import edu.washington.cs.workflow.instrument.InstruPointsController;
import edu.washington.cs.workflow.instrument.InstrumentStats;
import edu.washington.cs.workflow.util.Globals;
import edu.washington.cs.workflow.util.Utils;

public class UIActionTraceInstrumenter extends AbstractInstrumenter {

    public final InstruPointsController controller; 
    
    static final String fieldName = "UI_action_tracer_enabled";
	static final String tracerFieldName = "tracer";
	static final String traceEventHandlerEntry = "traceEventHandlerEntry";
	static final String traceEventHandlerExit = "traceEventHandlerExit";
	static final String traceAppMethodEntry = "traceAppMethodEntry";
	static final String traceAppMethodExit = "traceAppMethodExit";
	
	/* correspond to: tracer = UIActionTracer.tracer; */
	static final Instruction getTracer = Util.makeGet(UIActionTracer.class, tracerFieldName);
	
	/* the instruction for invoking recording method */
	static final Instruction recordHandlerEntry = Util.makeInvoke(UIActionTracer.class, traceEventHandlerEntry, new Class[] { String.class, Object.class});
	static final Instruction recordHandlerExit = Util.makeInvoke(UIActionTracer.class, traceEventHandlerExit, new Class[] { String.class, Object.class});
	static final Instruction recordMethodEntry = Util.makeInvoke(UIActionTracer.class, traceAppMethodEntry, new Class[] { String.class, Object.class});
	static final Instruction recordMethodExit = Util.makeInvoke(UIActionTracer.class, traceAppMethodExit, new Class[] { String.class, Object.class});
	
	public UIActionTraceInstrumenter(InstruPointsController controller) {
		Utils.checkNotNull(controller);
		this.controller = controller;
	}
	
	@Override
	protected void doClass(ClassInstrumenter ci, Writer w) throws Exception {
		final String className = ci.getReader().getName();
		w.write("Class: " + className + "\n");
		w.flush();

		for (int m = 0; m < ci.getReader().getMethodCount(); m++) {
			final MethodData d = ci.visitMethod(m);
			// d could be null, e.g., if the method is abstract or native
			if (d != null) {
				//skip empty methods, e.g., some event handler method is not override, but left as empty
				if(d.getInstructions().length == 1 && (d.getInstructions()[0] instanceof ReturnInstruction)) {
					continue;
				}
				//skip the synthetic method
				if(controller.isSyntheticMethodByHeuristic(d)) {
					continue;
				}
				if(controller.isMethodSkippable(d)) {
					continue;
				}
				w.write("Instrumenting " + ci.getReader().getMethodName(m) + " " + ci.getReader().getMethodType(m) + ":\n");
				w.flush();
				if (disasm) {
					w.write("Initial ShrikeBT code:\n");
					(new Disassembler(d)).disassembleTo(w);
					w.flush();
				}
				if (verify) {
					Verifier v = new Verifier(d);
					v.verify();
				}

				MethodEditor me = new MethodEditor(d);
				me.beginPass();

				// the unique method signature
				final String methodSig = this.getMethodSignature(d);
				//instrument event handling method
				if (this.controller.isEventHandlingMethod(d)) {
					me.insertAtStart(new MethodEditor.Patch() {
						@Override
						public void emitTo(MethodEditor.Output w) {
							Utils.debugPrintln("Instrument entry of event handler: " + methodSig);
							w.emit(getTracer);
							w.emit(ConstantInstruction.makeString(methodSig));
							if(!controller.isInitMethod(d) && !controller.isStaticMethod(d)) { 
							    w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
							} else {
								w.emit(ConstantInstruction.make(ConstantInstruction.TYPE_null, null));
							}
							w.emit(recordHandlerEntry);
							// keep the statistics
							InstrumentStats.addInsertedInstructions(1);
						}
					});
					IInstruction[] instr = me.getInstructions();
		            for (int i = 0; i < instr.length; i++) {
		                if (instr[i] instanceof ReturnInstruction) {
		                  me.insertBefore(i, new MethodEditor.Patch() {
		                    @Override
		                    public void emitTo(MethodEditor.Output w) {
		                      Utils.debugPrintln("Instrument return of event handler: " + methodSig);
		                      w.emit(getTracer);
		                      w.emit(ConstantInstruction.makeString(methodSig));
		                      if(!controller.isInitMethod(d) && !controller.isStaticMethod(d)) { 
							    w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
							  } else {
								w.emit(ConstantInstruction.make(ConstantInstruction.TYPE_null, null));
							  }
		                      w.emit(recordHandlerExit);
		                      //keep the statistics
		                      InstrumentStats.addInsertedInstructions(1);
		                    }
		                  });
		                }
		            }
		            me.addMethodExceptionHandler(null, new MethodEditor.Patch() {
		                  @Override
		                  public void emitTo(Output w) {
		                  	Utils.debugPrintln("Instrument the exception handling part of event handler: " + methodSig);
		                      w.emit(getTracer);
		                      w.emit(ConstantInstruction.makeString(methodSig));
		                      if(!controller.isInitMethod(d) && !controller.isStaticMethod(d)) { 
							    w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
							  } else {
								w.emit(ConstantInstruction.make(ConstantInstruction.TYPE_null, null));
							  }
		                      w.emit(recordHandlerExit);
		                      //keep the statistics
		                      InstrumentStats.addInsertedInstructions(1);
		                      //must rethrow the exception
		                      w.emit(ThrowInstruction.make(false));
		                  }
		                });

					// this updates the data, instrumentation ends
					me.applyPatches();
					if (disasm) {
						w.write("Final ShrikeBT code:\n");
						(new Disassembler(d)).disassembleTo(w);
						w.write(Globals.lineSep);
						w.flush();
					}
				}
				
				//instrument the method callers
				if (!this.controller.isEventHandlingMethod(d) && this.controller.isAppMethodToInstrument(d)) {
					me.insertAtStart(new MethodEditor.Patch() {
						@Override
						public void emitTo(MethodEditor.Output w) {
							Utils.debugPrintln("Instrument entry of method: " + methodSig);
							w.emit(getTracer);
							w.emit(ConstantInstruction.makeString(methodSig));
							if(!controller.isInitMethod(d) && !controller.isStaticMethod(d)) { 
							    w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
							} else {
								w.emit(ConstantInstruction.make(ConstantInstruction.TYPE_null, null));
							}
							w.emit(recordMethodEntry);
							// keep the statistics
							InstrumentStats.addInsertedInstructions(1);
						}
					});
					IInstruction[] instr = me.getInstructions();
		            for (int i = 0; i < instr.length; i++) {
		                if (instr[i] instanceof ReturnInstruction) {
		                  me.insertBefore(i, new MethodEditor.Patch() {
		                    @Override
		                    public void emitTo(MethodEditor.Output w) {
		                      Utils.debugPrintln("Instrument return of method: " + methodSig);
		                      w.emit(getTracer);
		                      w.emit(ConstantInstruction.makeString(methodSig));
		                      if(!controller.isInitMethod(d) && !controller.isStaticMethod(d)) { 
							    w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
							  } else {
								w.emit(ConstantInstruction.make(ConstantInstruction.TYPE_null, null));
							  }
		                      w.emit(recordMethodExit);
		                      //keep the statistics
		                      InstrumentStats.addInsertedInstructions(1);
		                    }
		                  });
		              }
		            }
		            me.addMethodExceptionHandler(null, new MethodEditor.Patch() {
		                  @Override
		                  public void emitTo(Output w) {
		                  	Utils.debugPrintln("Instrument the exception handling part of method: " + methodSig);
		                      w.emit(getTracer);
		                      w.emit(ConstantInstruction.makeString(methodSig));
		                      if(!controller.isInitMethod(d) && !controller.isStaticMethod(d)) { 
							    w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
							  } else {
								w.emit(ConstantInstruction.make(ConstantInstruction.TYPE_null, null));
							  }
		                      w.emit(recordMethodExit);
		                      //keep the statistics
		                      InstrumentStats.addInsertedInstructions(1);
		                      //must rethrow the exception
		                      w.emit(ThrowInstruction.make(false));
		                }
		            });

					// this updates the data, instrumentation ends
					me.applyPatches();
					if (disasm) {
						w.write("Final ShrikeBT code:\n");
						(new Disassembler(d)).disassembleTo(w);
						w.write(Globals.lineSep);
						w.flush();
					}
				}
			}
		}
		//add a class field
	    if (ci.isChanged()) {
	      ClassWriter cw = ci.emitClass();
	      cw.addField(ClassReader.ACC_PUBLIC | ClassReader.ACC_STATIC, fieldName, Constants.TYPE_boolean, new ClassWriter.Element[0]);
	      instrumenter.outputModifiedClass(ci, cw);
	    }
	}
}